﻿/*
Bf3Rcon.NET, provides a .NET implementation of BF3's RCON interface.
Copyright (C) 2011 agentwite, Timi, Unseen, AlCapwn

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

You can contact us at http://bf3rcon.codeplex.com/.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;

namespace System.Net.Battlefield3
{
    /// <summary>
    /// Provides a wrapper around RCON's banlist functionality, allowing enumeration and manipulation of the banlist, as
    /// well as the ability to issue and remove bans.
    /// </summary>
    public sealed class BanCollection
        : MarshalByRefObject, IEnumerable<Ban>
    {
        const int MaxPageSize = 100;
        const int WordsPerBan = 6;
        RconClientBase Client;

        internal BanCollection(RconClientBase client)
        {
            Client = client;
        }

        #region Helper methods
        Packet GetPacket(int offset)
        {
            return Client.InternalSendRequest("banList.list", offset.ToStringG());
        }

        //parses the ban at the offset. assume offset is valid
        static Ban ParseBan(Packet p, int offset) //offset is the nth ban of the packet
        {
            //set the offset to now be the nth word
            offset = offset * WordsPerBan + 1; //1 for OK
            ReadOnlyCollection<string> w = p.Words;

            return new Ban(GetTargetType(w[offset++]), w[offset++], GetDurationType(w[offset++]),
                GetDuration(w, ref offset), w[offset++]);
        }

        //this will use yield and therefore be more efficient than receiving all of the bans first
        IEnumerable<Ban> ParseBans()
        {
            int offset = 0;
            Packet p = GetPacket(offset);
            while (p.Success() && p.Words.Count > 1) //packet has bans
            {
                offset += (p.Words.Count - 1) / WordsPerBan;

                for (int i = 1; i < p.Words.Count;)
                {
                    yield return new Ban(
                        GetTargetType(p.Words[i++]), p.Words[i++], GetDurationType(p.Words[i++]),
                        GetDuration(p.Words, ref i), p.Words[i++]);
                }

                p = GetPacket(offset);
            }
            yield break;
        }

        #region Parses ban parts
        static BanTargetType GetTargetType(string str)
        {
            switch (str)
            {
                case "guid": return BanTargetType.Guid;
                case "ip": return BanTargetType.IPAddress;
                case "name": return BanTargetType.Name;
                default: throw new ArgumentException("A weird, lolzy exception occurred in BanCollection.", "str"); //this should never happen, though
            }
        }

        static string GetTargetTypeString(BanTargetType targetType)
        {
            switch (targetType)
            {
                case BanTargetType.IPAddress:
                    return "ip";
                case BanTargetType.Name:
                    return "name";
                case BanTargetType.Guid:
                    return "guid";
                default:
                    throw new ArgumentException("A weird, lolzy exception occurred in BanCollection.", "targetType"); //wont happen
            }
        }

        static BanDurationType GetDurationType(string str)
        {
            switch (str)
            {
                case "perm": return BanDurationType.Permanent;
                case "seconds": return BanDurationType.Seconds;
                case "rounds": return BanDurationType.Rounds;
                default: throw new ArgumentException("A weird, lolzy exception occurred in BanCollection.", "str"); //this should never happen, though
            }
        }

        static string GetDurationTypeString(BanDurationType durationType)
        {
            switch (durationType)
            {
                case BanDurationType.Seconds:
                    return "seconds";
                case BanDurationType.Rounds:
                    return "rounds";
                case BanDurationType.Permanent:
                    return "perm";
                default:
                    throw new ArgumentException("A weird, lolzy exception occurred in BanCollection.", "durationType"); //wont happen
            }
        }

        //gets the duration and increments the offset to the correct value to next get reason
        static int GetDuration(ReadOnlyCollection<string> w, ref int offset) //offset is the seconds part of the ban
        {
            offset += 2; //just set the offset now so it can just return
            if (w[offset - 2] == "0") //since expired bans shouldn't be in the banlist, this means it's not a time ban
                return w[offset - 1].ToInt32(); //so return the next word, which should be correct

            return w[offset - 2].ToInt32(); //otherwise, just return that first word
        }
        #endregion

        #endregion

        /// <summary>
        /// Gets the <see cref="Ban"/> at the specified <paramref name="index"/>.
        /// </summary>
        /// <param name="index">The zero-based index of the <see cref="Ban"/> to get.</param>
        /// <returns>The <see cref="Ban"/> at that index or null if that index doesn't exist or the query timed out.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <remarks>
        /// An <see cref="ArgumentOutOfRangeException"/> is not thrown here because there's no property
        /// provided to determine the number of <see cref="Ban">Bans</see>.
        /// </remarks>
        public Ban this[int index]
        {
            get
            {
                if (!Client.IsLoggedOn) throw new NotLoggedOnException();

                Packet p = GetPacket(index);

                if (p == null || p.Words.Count == 1) return null;

                return ParseBan(p, 0);
            }
        }

        /// <summary>
        /// Adds a <see cref="Ban"/> to the <see cref="BanCollection"/>.
        /// </summary>
        /// <param name="ban">The <see cref="Ban"/> that will be added.</param>
        /// <returns>True if the <see cref="Ban"/> was successfully added.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="ban"/> must not be null.</exception>
        public bool Add(Ban ban)
        {
            if (!Client.IsLoggedOn) throw new NotLoggedOnException();

            if (ban == null) throw new ArgumentNullException("ban", "ban must not be null.");

            List<string> banWords = new List<string>(6) {"banList.add"}; //6 is max

            banWords.Add(GetTargetTypeString(ban.TargetType));
            banWords.Add(ban.Target);
            banWords.Add(GetDurationTypeString(ban.DurationType));
            if (ban.DurationType != BanDurationType.Permanent)
                banWords.Add(ban.Duration.ToStringG());
            if (ban.Reason.HasValue()) banWords.Add(ban.Reason);

            return Client.InternalSendRequest(banWords.ToArray()).Success();
        }

        /// <summary>
        /// Removes a <see cref="Ban"/> from the <see cref="BanCollection"/>.
        /// </summary>
        /// <param name="ban">The <see cref="Ban"/> that will be removed.</param>
        /// <returns>True if the <see cref="Ban"/> was successfully removed.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool Remove(Ban ban)
        {
            if (!Client.IsLoggedOn) throw new NotLoggedOnException();

            return Client.InternalSendRequest("banList.remove", GetTargetTypeString(ban.TargetType), ban.Target).Success();
        }

        /// <summary>
        /// Clears all <see cref="Ban">Bans</see> from the <see cref="BanCollection"/>.
        /// </summary>
        /// <returns>True if the <see cref="BanCollection"/> was successfully cleared.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool Clear()
        {
            if (!Client.IsLoggedOn) throw new NotLoggedOnException();

            return Client.InternalSendRequest("banList.clear").Success();
        }

        #region Banning methods
        /// <summary>
        /// Issues a permanent <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool PermanentBan(Player player)
        {
            return Add(Ban.PermanentBan(player));
        }

        /// <summary>
        /// Issues a time <see cref="Ban"/> (in seconds) for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="seconds">The number of seconds the <see cref="Player"/> will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must be greater than zero.</exception>
        public bool TimeBan(Player player, int seconds)
        {
            if (seconds < 1) throw new ArgumentOutOfRangeException("seconds", "seconds must be greater than zero.");

            return Add(Ban.TimeBan(player, seconds));
        }

        /// <summary>
        /// Issues a round <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="rounds">The number of rounds the <see cref="Player"/> will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="rounds"/> must be greater than zero.</exception>
        public bool RoundBan(Player player, int rounds)
        {
            if (rounds < 1) throw new ArgumentOutOfRangeException("rounds", "rounds must be greater than zero.");

            return Add(Ban.RoundBan(player, rounds));
        }

        //These add reasons. was simple copy and paste

        /// <summary>
        /// Issues a permanent <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool PermanentBan(Player player, string reason)
        {
            return Add(Ban.PermanentBan(player, reason));
        }

        /// <summary>
        /// Issues a time <see cref="Ban"/> (in seconds) for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="seconds">The number of seconds the <see cref="Player"/> will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must be greater than zero.</exception>
        public bool TimeBan(Player player, int seconds, string reason)
        {
            if (seconds < 1) throw new ArgumentOutOfRangeException("seconds", "seconds must be greater than zero.");

            return Add(Ban.TimeBan(player, seconds, reason));
        }

        /// <summary>
        /// Issues a round <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="rounds">The number of rounds the <see cref="Player"/> will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="rounds"/> must be greater than zero.</exception>
        public bool RoundBan(Player player, int rounds, string reason)
        {
            if (rounds < 1) throw new ArgumentOutOfRangeException("rounds", "rounds must be greater than zero.");

            return Add(Ban.RoundBan(player, rounds, reason));
        }

        //These now allow a customizable bantargetype, another simple copy and paste

        /// <summary>
        /// Issues a permanent <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool PermanentBan(Player player, BanTargetType targetType)
        {
            return Add(Ban.PermanentBan(player, targetType));
        }

        /// <summary>
        /// Issues a time <see cref="Ban"/> (in seconds) for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="seconds">The number of seconds the <see cref="Player"/> will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must be greater than zero.</exception>
        public bool TimeBan(Player player, int seconds, BanTargetType targetType)
        {
            if (seconds < 1) throw new ArgumentOutOfRangeException("seconds", "seconds must be greater than zero.");

            return Add(Ban.TimeBan(player, targetType, seconds));
        }

        /// <summary>
        /// Issues a round <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="rounds">The number of rounds the <see cref="Player"/> will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="rounds"/> must be greater than zero.</exception>
        public bool RoundBan(Player player, int rounds, BanTargetType targetType)
        {
            if (rounds < 1) throw new ArgumentOutOfRangeException("rounds", "rounds must be greater than zero.");

            return Add(Ban.RoundBan(player, targetType, rounds));
        }

        /// <summary>
        /// Issues a permanent <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        public bool PermanentBan(Player player, BanTargetType targetType, string reason)
        {
            return Add(Ban.PermanentBan(player, targetType, reason));
        }

        /// <summary>
        /// Issues a time <see cref="Ban"/> (in seconds) for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="seconds">The number of seconds the <see cref="Player"/> will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="seconds"/> must be greater than zero.</exception>
        public bool TimeBan(Player player, int seconds, BanTargetType targetType, string reason)
        {
            if (seconds < 1) throw new ArgumentOutOfRangeException("seconds", "seconds must be greater than zero.");

            return Add(Ban.TimeBan(player, targetType, seconds, reason));
        }

        /// <summary>
        /// Issues a round <see cref="Ban"/> for a <see cref="Player"/>, which will ban the <see cref="Player.Guid"/>.
        /// </summary>
        /// <param name="player">The <see cref="Player"/> that will be banned.</param>
        /// <param name="rounds">The number of rounds the <see cref="Player"/> will be banned.</param>
        /// <param name="targetType">The <see cref="BanTargetType"/> that indicates what about the <see cref="Player"/> will be banned.</param>
        /// <param name="reason">The reason the <see cref="Player"/> is being banned.</param>
        /// <returns>True if the <paramref name="player"/> is successfully banned.</returns>
        /// <exception cref="NotLoggedOnException">The <see cref="RconClient"/> isn't logged on.</exception>
        /// <exception cref="ArgumentOutOfRangeException"><paramref name="rounds"/> must be greater than zero.</exception>
        public bool RoundBan(Player player, int rounds, BanTargetType targetType, string reason)
        {
            if (rounds < 1) throw new ArgumentOutOfRangeException("rounds", "rounds must be greater than zero.");

            return Add(Ban.RoundBan(player, targetType, rounds, reason));
        }
        #endregion

        /// <summary>
        /// Returns an enumerator that iterates through the <see cref="BanCollection"/>.
        /// </summary>
        /// <returns>An <see cref="T:System.Collections.Generic.IEnumerator`1">IEnumerator(Ban)</see> that iterates through the <see cref="BanCollection"/>.</returns>
        public IEnumerator<Ban> GetEnumerator()
        {
            return ParseBans().GetEnumerator();
        }

        /// <summary>
        /// Returns an enumerator that iterates through the <see cref="BanCollection"/>.
        /// </summary>
        /// <returns>An <see cref="T:System.Collections.IEnumerator"/> that iterates through the <see cref="BanCollection"/>.</returns>
        Collections.IEnumerator Collections.IEnumerable.GetEnumerator()
        {
            return ParseBans().GetEnumerator();
        }
    }
}
